在 Day 28 中,我們剖析了移軸微縮景深、電影暗角以及 0.98 極限門檻的 UnrealBloomPass,徹底抹除了 WebGL 針孔相機的冰冷數位感,將這座地下城淬鍊成了玩具屋般的精緻畫面。
至此,我們所有的視覺渲染模組已經完全建構完成。然而,許多 Web 3D 開發者在準備發布專案時,往往會遭遇最殘酷的「現實撞擊」:
在自己的高階電競桌機上跑得順暢無比(60 FPS),一換到配備 Retina 螢幕的 MacBook 或是 4K 螢幕上,風扇狂轉、畫面直接掉到 20 FPS;
使用者隨手拖拉一下瀏覽器視窗大小,畫面不是被拉扯變形,就是後處理濾鏡產生嚴重的模糊與像素撕裂;
手機背景切換或螢幕休眠喚醒後,畫面直接變成一片死黑,控制台跳出神祕的報錯訊息。
傳統原生遊戲(如 Unreal、Unity)能擁有獨立的執行檔與整台電腦的硬體支配權;但 Three.js 與 WebGL 卻是運行在瀏覽器沙盒之中。這意味著我們必須面對解析度懸殊、視窗動態變換與顯存隨時可能被作業系統回收的嚴苛環境。
今天作為【技術精講篇】的最後一站,我們將跳出單純的視覺效果,探討如何跨越 Web 平台的硬體限制,在極致畫質與 60 FPS 效能之間取得完美的工程平衡!
在開發現代網頁時,很多教學會告訴你這行「標準配置」:
// 看似理所當然,實則在 4K/Retina 設備上極度危險的寫法
renderer.setPixelRatio(window.devicePixelRatio);
在標準的 1080p 螢幕上,CSS 像素與實體硬體像素比例是 $1:1$(devicePixelRatio = 1)。畫布繪製 1920x1080,大約是 207 萬個像素。但在 iPhone、高階 Android 手機或 MacBook 的 Retina 螢幕上,devicePixelRatio 通常高達 2.0 甚至 3.0:
如果我們只用原生渲染畫一個簡單方塊,GPU 或許還扛得住;但別忘了我們的專案掛載了 EffectComposer 後處理管線:
這意味著在每一幀的 16.6 毫秒內,GPU 的片段著色器必須對這 800~1800 萬個像素進行數十次重複讀寫!顯卡的記憶體頻寬與像素填充率瞬間被榨乾,這就是為什麼高解析度螢幕會突然出現斷崖式掉幀。
在 HD-2D 的世界裡,我們的核心魅力在於「復古像素顆粒感」。強行在 4K 實體解析度下進行次像素渲染,對 16x16 的像素藝術並不會帶來畫質上的質變,反而白白犧牲了流暢度。
我們在初始化時採用了動態鉗制策略:
// 將像素比嚴格限制在 2.0 以內 (低階裝置甚至可限制在 1.5)
const pixelRatio = Math.min(window.devicePixelRatio, 2.0);
renderer.setPixelRatio(pixelRatio);
composer.setPixelRatio(pixelRatio);
透過將上限死鎖在 2.0,我們既消除了高階螢幕上的文字與線條鋸齒,又將 GPU 的計算負擔精準控制在安全紅線之內,杜絕了填充率雪崩。
當使用者拖曳瀏覽器邊框、點擊全螢幕切換,或在行動裝置上旋轉橫豎螢幕時,畫面尺寸會即時變更。許多開發者雖然更新了相機長寬比,畫面卻依然變形或模糊。
一個完整的 Web 3D 渲染系統在視窗尺寸改變時,必須依序同步三個層級:
如果只更新了 renderer.setSize(),而漏掉了 composer.setSize(),後處理的多重離屏緩衝區(Render Target)將會繼續維持原本的解析度,導致畫面被硬生生拉伸放大,移軸散景與暗角的焦點也會徹底偏離。
window.addEventListener('resize', () => {
const width = window.innerWidth;
const height = window.innerHeight;
// 1. 更新相機長寬比與投影矩陣
camera.aspect = width / height;
camera.updateProjectionMatrix();
// 2. 同步更新渲染器畫布
renderer.setSize(width, height);
// 3. 核心:同步更新 EffectComposer 內部所有的 Ping-Pong 緩衝區!
composer.setSize(width, height);
// 4. 更新自研移軸著色器的全螢幕尺寸 Uniform (確保散景半徑精準一致)
if (tiltShiftPass) {
tiltShiftPass.uniforms.h.value = 1.0 / height;
}
});
在傳統軟體中,顯卡驅動程式不會隨意關閉你的畫面;但在瀏覽器中,WebGL 是受到嚴格監控的共享資源。
遇到這些情況時,瀏覽器會直接強制銷毀當前的 WebGL 上下文,控制台會拋出 THREE.WebGLRenderer: Context Lost. 警告,畫布瞬間凍結或翻黑。
商業級的 Web 應用程式絕不能讓使用者在喚醒電腦後看著黑畫面發呆。我們可以透過標準的 WebGL 事件監聽建立防禦機制:
const canvas = renderer.domElement;
// 1. 監聽上下文遺失事件
canvas.addEventListener('webglcontextlost', (event) => {
// 核心:阻止瀏覽器預設的放棄行為,告知我們將自行處理復原!
event.preventDefault();
// 暫停 requestAnimationFrame 主渲染迴圈,停止空轉調用
cancelAnimationFrame(animationFrameId);
console.warn('WebGL 上下文暫時遺失,渲染迴圈已安全掛起。');
}, false);
// 2. 監聽上下文復原事件
canvas.addEventListener('webglcontextrestored', () => {
console.log('WebGL 上下文已復原,重新編譯著色器並重啟管線...');
// 重新初始化材質與後處理緩衝區
initPipeline();
// 重啟渲染迴圈
animate();
}, false);
透過這層容錯包裝,即便使用者蓋上筆電休眠數小時後重新打開,地宮也能在幾毫秒內靜默完成顯存重建,繼續流暢運行。
在深入探討了 Web 平台的種種限制後,我們更應該看見它無可取代的巨大優勢。
傳統 3D 遊戲動輒需要下載數十 GB 的安裝包,包含龐大的 3D 模型網格、4K PBR 貼圖與預烘焙光照貼圖。這在網頁環境中是不可接受的,因為使用者在網頁上只願意等待 2 到 3 秒。
回顧我們整套專案的資源架構:
極度輕量的幾何資料:地宮的 2,500 個網格、牆面與走廊,全部是在客戶端透過簡單的二維矩陣與原生方塊即時生成,不佔用任何網路下載頻寬。
極致壓縮的貼圖體積:核心視覺全由幾張 16x16、32x32 的復古像素圖元(Sprite Sheet)組成,整體靜態資源體積不到 2 MB!
把運算交給 GPU 即時完成:
這種「以輕量資料為骨架、靠 WebGL 著色器在瀏覽器中即時演算」的架構,讓使用者只要點開一個網址,就能在不到一秒鐘的時間內,於任何瀏覽器視窗中即時走入這座深邃、華麗且散發著溫潤火光的 3D 地下城!
【本日結語】
今天我們為【技術精講篇】畫上了圓滿的句點:
從最初的一塊石磚、一盞火把,到深度比對軟陰影、Billboard 矩陣解耦、ACES 色彩科學、空間透視法則與鏡頭後處理,我們一步一腳印,用最純粹的 Three.js 與 WebGL 程式碼,在瀏覽器中重現了現代圖形學中最迷人的視覺魔法。
在明天的 Day 30——全系列完結特輯中,我們將迎來最後的旅程全景回顧!我們將重構全系統渲染架構大圖、盤點 30 天裡最棘手的踩坑血淚,並展望這套 Web 3D 技術邁向商業級作品的無限可能。我們明天最後一集見!